/* -------------------------------------------------------------------------
 *
 * contrib/sepgsql/uavc.c
 *
 * 用户空间访问向量缓存的实现；可以缓存最近使用的访问控制决策，
 * 并减少 kernel 调用的数量以避免不必要的性能损失。
 *
 * Copyright (c) 2011-2022, PostgreSQL Global Development Group
 *
 * -------------------------------------------------------------------------
 */
#include "postgres.h"

#include "catalog/pg_proc.h"
#include "commands/seclabel.h"
#include "common/hashfn.h"
#include "sepgsql.h"
#include "storage/ipc.h"
#include "utils/guc.h"
#include "utils/memutils.h"

/*
 * avc_cache
 *
 * 它使得能够缓存特定安全标签和对象类别在用户空间的访问控制决策（以及在执行受信任程序时的行为，仅限于db_procedure类）。
 */
typedef struct
{
	uint32		hash;			/* 这个缓存条目的哈希值 */
	char	   *scontext;		/* 主体的安全上下文 */
	char	   *tcontext;		/* 目标的安全上下文 */
	uint16		tclass;			/* 目标的对象类别 */

	uint32		allowed;		/* 允许的权限 */
	uint32		auditallow;		/* 允许时要审计的权限 */
	uint32		auditdeny;		/* 拒绝时要审计的权限 */

	bool		permissive;		/* 如果是宽松规则，则为真 */
	bool		hot_cache;		/* 如果最近被引用，则为真 */
	bool		tcontext_is_valid;
	/* 如果tcontext有效，则为真 */
	char	   *ncontext;		/* 在执行受信任程序时的临时scontext，或在其他地方为NULL */
}			avc_cache;

/*
 * 静态变量的声明
 */
#define AVC_NUM_SLOTS		512
#define AVC_NUM_RECLAIM		16
#define AVC_DEF_THRESHOLD	384

static MemoryContext avc_mem_cxt;
static List *avc_slots[AVC_NUM_SLOTS];	/* avc的哈希桶 */
static int	avc_num_caches;		/* 当前使用的缓存数量 */
static int	avc_lru_hint;		/* 下一个要回收的桶的索引 */
static int	avc_threshold;		/* 启动缓存回收的阈值 */
static char *avc_unlabeled;		/* 系统“未标记”标签 */

/*
 * 哈希函数
 */
static uint32 fc_sepgsql_avc_hash(const char *fc_scontext, const char *fc_tcontext, uint16 fc_tclass)
{
	return hash_any((const unsigned char *) fc_scontext, strlen(fc_scontext))
		^ hash_any((const unsigned char *) fc_tcontext, strlen(fc_tcontext))
		^ fc_tclass;
}

/*
 * 重置所有avc缓存
 */
static void fc_sepgsql_avc_reset(void)
{
	MemoryContextReset(avc_mem_cxt);

	memset(avc_slots, 0, sizeof(List *) * AVC_NUM_SLOTS);
	avc_num_caches = 0;
	avc_lru_hint = 0;
	avc_unlabeled = NULL;
}

/*
 * 回收最近未被引用的缓存
 */
static void fc_sepgsql_avc_reclaim(void)
{
	ListCell   *fc_cell;
	int			fc_index;

	while (avc_num_caches >= avc_threshold - AVC_NUM_RECLAIM)
	{
		fc_index = avc_lru_hint;

		foreach(fc_cell, avc_slots[fc_index])
		{
			avc_cache  *fc_cache = lfirst(fc_cell);

			if (!fc_cache->hot_cache)
			{
				avc_slots[fc_index]
					= foreach_delete_current(avc_slots[fc_index], fc_cell);

				pfree(fc_cache->scontext);
				pfree(fc_cache->tcontext);
				if (fc_cache->ncontext)
					pfree(fc_cache->ncontext);
				pfree(fc_cache);

				avc_num_caches--;
			}
			else
			{
				fc_cache->hot_cache = false;
			}
		}
		avc_lru_hint = (avc_lru_hint + 1) % AVC_NUM_SLOTS;
	}
}

/* -------------------------------------------------------------------------
 *
 * sepgsql_avc_check_valid
 *
 * 此函数检查缓存条目是否仍然有效。如果自上次引用访问向量缓存以来，安全策略已被重新加载（或发生任何其他需要重置用户空间缓存的事件），我们必须刷新缓存。
 *
 * 访问控制决策必须是原子性的，但可能需要多个系统调用来做出决策；因此，在引用访问向量缓存时，我们必须循环，直到在没有中介缓存刷新事件的情况下完成。在实际操作中，循环一次应该是非常稀有的。调用者应该做类似以下的事情：
 *
 *	 sepgsql_avc_check_valid();
 *	 do {
 *			 :
 *		 <引用uavc>
 *			 :
 *	 } while (!sepgsql_avc_check_valid())
 *
 * -------------------------------------------------------------------------
 */
static bool fc_sepgsql_avc_check_valid(void)
{
	if (selinux_status_updated() > 0)
	{
		fc_sepgsql_avc_reset();

		return false;
	}
	return true;
}

/*
 * sepgsql_avc_unlabeled
 *
 * 当没有标签或分配无效标签时，返回一个替代标签。
 */
static char * fc_sepgsql_avc_unlabeled(void)
{
	if (!avc_unlabeled)
	{
		char	   *fc_unlabeled;

		if (security_get_initial_context_raw("unlabeled", &fc_unlabeled) < 0)
			ereport(ERROR,
					(errcode(ERRCODE_INTERNAL_ERROR),
					 errmsg("SELinux: failed to get initial security label: %m")));
		PG_TRY();
		{
			avc_unlabeled = MemoryContextStrdup(avc_mem_cxt, fc_unlabeled);
		}
		PG_FINALLY();
		{
			freecon(fc_unlabeled);
		}
		PG_END_TRY();
	}
	return avc_unlabeled;
}

/*
 * sepgsql_avc_compute
 *
 * 当缓存未命中时的后备路径。它请求SELinux针对提供的安全上下文和对象类别对其访问控制决策。
 */
static avc_cache *
fc_sepgsql_avc_compute(const char *fc_scontext, const char *fc_tcontext, uint16 fc_tclass)
{
	char	   *fc_ucontext = NULL;
	char	   *fc_ncontext = NULL;
	MemoryContext fc_oldctx;
	avc_cache  *fc_cache;
	uint32		fc_hash;
	int			fc_index;
	struct av_decision fc_avd;

	fc_hash = fc_sepgsql_avc_hash(fc_scontext, fc_tcontext, fc_tclass);
	fc_index = fc_hash % AVC_NUM_SLOTS;

	/*
	 * 对提供的安全上下文进行验证检查。因为它总是调用系统调用，所以应避免频繁检查。除非安全策略被重新加载，否则验证状态应保持，因此我们还缓存提供的安全上下文是否有效。
	 */
	if (security_check_context_raw(fc_tcontext) != 0)
		fc_ucontext = fc_sepgsql_avc_unlabeled();

	/*
	 * 请求SELinux的访问控制决策
	 */
	if (!fc_ucontext)
		sepgsql_compute_avd(fc_scontext, fc_tcontext, fc_tclass, &fc_avd);
	else
		sepgsql_compute_avd(fc_scontext, fc_ucontext, fc_tclass, &fc_avd);

	/*
	 * 它还缓存了一个安全标签，当一个标记为
	 * 'scontext' 的客户端执行一个标记为 'tcontext' 的过程时，将切换该标签，不仅对该过程的访问
	 * 控制决策。要切换的安全标签将根据 'scontext' 和 'tcontext' 的唯一组合进行计算，
	 * 因此，在 avc 上缓存新标签是合理的，并且能够
	 * 减少不必要的系统调用。它将在
	 * sepgsql_needs_fmgr_hook 中被引用，以检查提供的函数是否为
	 * 受信的过程。
	 */
	if (fc_tclass == SEPG_CLASS_DB_PROCEDURE)
	{
		if (!fc_ucontext)
			fc_ncontext = sepgsql_compute_create(fc_scontext, fc_tcontext,
											  SEPG_CLASS_PROCESS, NULL);
		else
			fc_ncontext = sepgsql_compute_create(fc_scontext, fc_ucontext,
											  SEPG_CLASS_PROCESS, NULL);
		if (strcmp(fc_scontext, fc_ncontext) == 0)
		{
			pfree(fc_ncontext);
			fc_ncontext = NULL;
		}
	}

	/*
	 * 设置一个 avc_cache 对象
	 */
	fc_oldctx = MemoryContextSwitchTo(avc_mem_cxt);

	fc_cache = palloc0(sizeof(avc_cache));

	fc_cache->hash = fc_hash;
	fc_cache->scontext = pstrdup(fc_scontext);
	fc_cache->tcontext = pstrdup(fc_tcontext);
	fc_cache->tclass = fc_tclass;

	fc_cache->allowed = fc_avd.allowed;
	fc_cache->auditallow = fc_avd.auditallow;
	fc_cache->auditdeny = fc_avd.auditdeny;
	fc_cache->hot_cache = true;
	if (fc_avd.flags & SELINUX_AVD_FLAGS_PERMISSIVE)
		fc_cache->permissive = true;
	if (!fc_ucontext)
		fc_cache->tcontext_is_valid = true;
	if (fc_ncontext)
		fc_cache->ncontext = pstrdup(fc_ncontext);

	avc_num_caches++;

	if (avc_num_caches > avc_threshold)
		fc_sepgsql_avc_reclaim();

	avc_slots[fc_index] = lcons(fc_cache, avc_slots[fc_index]);

	MemoryContextSwitchTo(fc_oldctx);

	return fc_cache;
}

/*
 * sepgsql_avc_lookup
 *
 * 查找与提供的安全上下文和
 * 对象类匹配的缓存条目。如果未找到，则创建一个新的缓存条目。
 */
static avc_cache *
fc_sepgsql_avc_lookup(const char *fc_scontext, const char *fc_tcontext, uint16 fc_tclass)
{
	avc_cache  *fc_cache;
	ListCell   *fc_cell;
	uint32		fc_hash;
	int			fc_index;

	fc_hash = fc_sepgsql_avc_hash(fc_scontext, fc_tcontext, fc_tclass);
	fc_index = fc_hash % AVC_NUM_SLOTS;

	foreach(fc_cell, avc_slots[fc_index])
	{
		fc_cache = lfirst(fc_cell);

		if (fc_cache->hash == fc_hash &&
			fc_cache->tclass == fc_tclass &&
			strcmp(fc_cache->tcontext, fc_tcontext) == 0 &&
			strcmp(fc_cache->scontext, fc_scontext) == 0)
		{
			fc_cache->hot_cache = true;
			return fc_cache;
		}
	}
	/* 未找到，因此插入新的缓存 */
	return fc_sepgsql_avc_compute(fc_scontext, fc_tcontext, fc_tclass);
}

/*
 * sepgsql_avc_check_perms(_label)
 *
 * 如果建议安全策略允许所需的
 * 权限，则返回 'true'。否则，返回 'false' 或根据
 * 'abort_on_violation' 参数引发错误。
 * 'tobject' 和 'tclass' 标识被引用的目标对象，
 * 'required' 是为每个
 * 对象类定义的权限位掩码 (SEPG_*__*)。
 * 'audit_name' 是对象名称（可选）。如果提供了 SEPGSQL_AVC_NOAUDIT，
 * 则表示跳过所有审计消息。
 */
bool sepgsql_avc_check_perms_label(const char *fc_tcontext,
							  uint16 fc_tclass, uint32 fc_required,
							  const char *fc_audit_name,
							  bool fc_abort_on_violation)
{
	char	   *fc_scontext = sepgsql_get_client_label();
	avc_cache  *fc_cache;
	uint32		fc_denied;
	uint32		fc_audited;
	bool		fc_result;

	fc_sepgsql_avc_check_valid();
	do
	{
		fc_result = true;

		/*
		 * 如果目标对象没有标签，我们将使用
		 * sepgsql_avc_unlabeled() 提供的标签进行检查。
		 */
		if (fc_tcontext)
			fc_cache = fc_sepgsql_avc_lookup(fc_scontext, fc_tcontext, fc_tclass);
		else
			fc_cache = fc_sepgsql_avc_lookup(fc_scontext,
									   fc_sepgsql_avc_unlabeled(), fc_tclass);

		fc_denied = fc_required & ~fc_cache->allowed;

		/*
		 * 计算要审计的权限
		 */
		if (sepgsql_get_debug_audit())
			fc_audited = (fc_denied ? (fc_denied & ~0) : (fc_required & ~0));
		else
			fc_audited = fc_denied ? (fc_denied & fc_cache->auditdeny)
				: (fc_required & fc_cache->auditallow);

		if (fc_denied)
		{
			/*
			 * 在宽容模式或宽容域中，违反的权限
			 * 应立即记录到日志文件中，然后隐式
			 * 允许以避免大量拒绝访问的日志，因为
			 * 宽容模式/域的目的是收集违规日志
			 * 以便能够修复安全策略。
			 */
			if (!sepgsql_getenforce() || fc_cache->permissive)
				fc_cache->allowed |= fc_required;
			else
				fc_result = false;
		}
	} while (!fc_sepgsql_avc_check_valid());

	/*
	 * 在这里有可审计的操作时，
	 * sepgsql_audit_log 应该使用主体和对象的安全
	 * 标签的文本表示被调用。它记录此访问
	 * 违规，因此 DBA 可以在后续过程中发现意外的安全问题。
	 */
	if (fc_audited != 0 &&
		fc_audit_name != SEPGSQL_AVC_NOAUDIT &&
		sepgsql_get_mode() != SEPGSQL_MODE_INTERNAL)
	{
		sepgsql_audit_log(fc_denied != 0,
						  (sepgsql_getenforce() && !fc_cache->permissive),
						  fc_cache->scontext,
						  fc_cache->tcontext_is_valid ?
						  fc_cache->tcontext : fc_sepgsql_avc_unlabeled(),
						  fc_cache->tclass,
						  fc_audited,
						  fc_audit_name);
	}

	if (fc_abort_on_violation && !fc_result)
		ereport(ERROR,
				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
				 errmsg("SELinux: security policy violation")));

	return fc_result;
}

bool sepgsql_avc_check_perms(const ObjectAddress *fc_tobject,
						uint16 fc_tclass, uint32 fc_required,
						const char *fc_audit_name,
						bool fc_abort_on_violation)
{
	char	   *fc_tcontext = GetSecurityLabel(fc_tobject, SEPGSQL_LABEL_TAG);
	bool		fc_rc;

	fc_rc = sepgsql_avc_check_perms_label(fc_tcontext,
									   fc_tclass, fc_required,
									   fc_audit_name, fc_abort_on_violation);
	if (fc_tcontext)
		pfree(fc_tcontext);

	return fc_rc;
}

/*
 * sepgsql_avc_trusted_proc
 *
 * 如果提供的函数 OID 被配置为受信程序，则该
 * 函数将返回在执行该函数期间使用的安全标签。
 * 否则，它返回 NULL。
 */
char * sepgsql_avc_trusted_proc(Oid fc_functionId)
{
	char	   *fc_scontext = sepgsql_get_client_label();
	char	   *fc_tcontext;
	ObjectAddress fc_tobject;
	avc_cache  *fc_cache;

	fc_tobject.classId = ProcedureRelationId;
	fc_tobject.objectId = fc_functionId;
	fc_tobject.objectSubId = 0;
	fc_tcontext = GetSecurityLabel(&fc_tobject, SEPGSQL_LABEL_TAG);

	fc_sepgsql_avc_check_valid();
	do
	{
		if (fc_tcontext)
			fc_cache = fc_sepgsql_avc_lookup(fc_scontext, fc_tcontext,
									   SEPG_CLASS_DB_PROCEDURE);
		else
			fc_cache = fc_sepgsql_avc_lookup(fc_scontext, fc_sepgsql_avc_unlabeled(),
									   SEPG_CLASS_DB_PROCEDURE);
	} while (!fc_sepgsql_avc_check_valid());

	return fc_cache->ncontext;
}

/*
 * sepgsql_avc_exit
 *
 * 在进程退出时清理用户空间 AVC。
 */
static void fc_sepgsql_avc_exit(int fc_code, Datum fc_arg)
{
	selinux_status_close();
}

/*
 * sepgsql_avc_init
 *
 * 初始化用户空间 AVC。这应该在 _PG_init 中被调用。
 */
void sepgsql_avc_init(void)
{
	int			fc_rc;

	/*
	 * 所有的 avc 相关内容应分配在 avc_mem_cxt 中
	 */
	avc_mem_cxt = AllocSetContextCreate(TopMemoryContext,
										"userspace access vector cache",
										ALLOCSET_DEFAULT_SIZES);
	memset(avc_slots, 0, sizeof(avc_slots));
	avc_num_caches = 0;
	avc_lru_hint = 0;
	avc_threshold = AVC_DEF_THRESHOLD;

	/*
	 * SELinux 允许以只读模式 mmap(2) 其内核状态页面，以
	 * 通知用户空间应用程序其状态更新（例如策略
	 * 重新加载）而无需系统调用。此功能仅在
	 * Linux-2.6.38 或更高版本中受支持，然而，libselinux 提供了一个
	 * 后备模式来通过 netlink 套接字获取其状态。
	 */
	fc_rc = selinux_status_open(1);
	if (fc_rc < 0)
		ereport(ERROR,
				(errcode(ERRCODE_INTERNAL_ERROR),
				 errmsg("SELinux: could not open selinux status : %m")));
	else if (fc_rc > 0)
		ereport(LOG,
				(errmsg("SELinux: kernel status page uses fallback mode")));

	/* 安排在进程退出时关闭 selinux 状态页面。 */
	on_proc_exit(fc_sepgsql_avc_exit, 0);
}
